﻿using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using Xenta.Entities;
using Xenta.Utils;

namespace Xenta
{
    /// <summary>
    /// Exposes the unit of work interface.
    /// </summary>
    /// <remarks>
    /// The IUnitOfWork interface provides the abstraction 
    /// from an ORM.
    /// </remarks>
    public interface IUnitOfWork : IDisposable
    {
        #region Properties

        /// <summary>
        /// Gets or sets the infrastructure instance.
        /// </summary>
        IInfrastructure Infrastructure
        {
            get;
            set;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates an entity in the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The entity identifier.</returns>
        object Create<TEntity>(TEntity entity, bool stateless = false)
            where TEntity : IEntity;

        /// <summary>
        /// Updates a persistent entity in the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        void Update<TEntity>(TEntity entity, bool stateless = false)
            where TEntity : IEntity;

        /// <summary>
        /// Removes a persistent entity from the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        void Delete<TEntity>(TEntity entity, bool stateless = false)
            where TEntity : IEntity;

        /// <summary>
        /// Gets an entity by the identifier.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entityID">The entity identifier.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The entity if it found in the datastore; otherwise null.</returns>
        TEntity Get<TEntity>(object entityID, bool stateless = false)
            where TEntity : IEntity;

        /// <summary>
        /// Loads an entity by the identifier.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entityID">The entity identifier.</param>
        /// <returns>The entity if it found in the datastore; otherwise throws exception.</returns>
        /// <remarks>The exception won't be thrown, if stateless mode is enabled.</remarks>
        TEntity Load<TEntity>(object entityID) 
            where TEntity : IEntity;
        
        /// <summary>
        /// Refresh an entity state.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        void Refresh<TEntity>(TEntity entity, bool stateless = false)
            where TEntity : IEntity;

        /// <summary>
        /// Merges an entity state.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        TEntity Merge<TEntity>(TEntity entity)
            where TEntity : class, IEntity;

        /// <summary>
        /// Query over the entity collection.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The queryable entity collection.</returns>
        IQueryable<TEntity> Query<TEntity>(bool stateless = false)
            where TEntity : IEntity;

        /// <summary>
        /// Creates an ORM query.
        /// </summary>
        /// <param name="queryString">The query string.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The ORM query.</returns>
        IQuery OrmQuery(string queryString, bool stateless = false);

        /// <summary>
        /// Creates a SQL query.
        /// </summary>
        /// <param name="queryString">The query string.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The SQL query.</returns>
        ISQLQuery SqlQuery(string queryString, bool stateless = false);

        /// <summary>
        /// Commits all previously executed work.
        /// </summary>
        void Commit();

        /// <summary>
        /// Flush the unit of work.
        /// </summary>
        void Flush();

        /// <summary>
        /// Rollbacks all previosly executed work.
        /// </summary>
        void Rollback();

        /// <summary>
        /// Suspends the unit.
        /// </summary>
        void Suspend();

        #endregion
    }

    /// <summary>
    /// The IUnitOfWork implementation.
    /// </summary>
    public class UnitOfWorkImpl : IUnitOfWork
    {
        #region Fields

        private readonly ISessionFactory _factory;
        private ISession _sfSession;
        private ITransaction _sfTransaction;
        private IStatelessSession _slSession;
        private ITransaction _slTransaction;

        #endregion

        #region Ctors

        /// <summary>
        /// Initializes a new instance of the class.
        /// </summary>
        /// <param name="factory">The NHibernate session factory.</param>
        public UnitOfWorkImpl(ISessionFactory factory)
        {
            if(factory == null)
                throw new ArgumentNullException("factory");
            _factory = factory;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the infrastructure.
        /// </summary>
        public IInfrastructure Infrastructure
        {
            get;
            set;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Commits all previously executed work.
        /// </summary>
        public void Commit()
        {
            var exceptions = new List<Exception>();

            if(_sfTransaction != null)
            {
                try
                {
                    _sfTransaction.Commit();
                }
                catch(Exception ex)
                {
                    exceptions.Add(ex);
                }
                finally
                {
                    _sfTransaction.Dispose();
                    _sfTransaction = null;
                }
            }
            if(_slTransaction != null)
            {
                try
                {
                    _slTransaction.Commit();
                }
                catch(Exception ex)
                {
                    exceptions.Add(ex);
                }
                finally
                {
                    _slTransaction.Dispose();
                    _slTransaction = null;
                }
            }
            if(exceptions.Any())
            {
                if(exceptions.Count == 1)
                    throw exceptions.Single();
                else
                    throw new AggregateException(exceptions);
            }
        }

        /// <summary>
        /// Flush the unit of work.
        /// </summary>
        public void Flush()
        {
            if(_sfSession == null)
                return;
            _sfSession.Flush();
        }

        /// <summary>
        /// Rollbacks all previosly executed work.
        /// </summary>
        public void Rollback()
        {
            var exceptions = new List<Exception>();

            if(_sfTransaction != null)
            {
                try
                {
                    _sfTransaction.Rollback();
                }
                catch(Exception ex)
                {
                    exceptions.Add(ex);
                }
                finally
                {
                    _sfTransaction.Dispose();
                    _sfTransaction = null;
                    _sfSession.Dispose();
                    _sfSession = null;
                }
            }
            if(_slTransaction != null)
            {
                try
                {
                    _slTransaction.Rollback();
                }
                catch(Exception ex)
                {
                    exceptions.Add(ex);
                }
                finally
                {
                    _slTransaction.Dispose();
                    _slTransaction = null;
                    _slSession.Dispose();
                    _slSession = null;
                }
            }
            if(exceptions.Any())
            {
                if(exceptions.Count == 1)
                    throw exceptions.Single();
                else
                    throw new AggregateException(exceptions);
            }
        }

        /// <summary>
        /// Creates an entity in the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The entity identifier.</returns>
        public object Create<TEntity>(TEntity entity, bool stateless)
            where TEntity : IEntity
        {
            BeginWork(stateless);
            if(stateless)
                return _slSession.Insert(entity);
            else
                return _sfSession.Save(entity);
        }

        /// <summary>
        /// Updates a persistent entity in the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        public void Update<TEntity>(TEntity entity, bool stateless)
            where TEntity : IEntity
        {
            BeginWork(stateless);
            if(stateless)
                _slSession.Update(entity);
            else
                _sfSession.Update(entity);
        }

        /// <summary>
        /// Removes a persistent entity from the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        public void Delete<TEntity>(TEntity entity, bool stateless)
            where TEntity : IEntity
        {
            BeginWork(stateless);
            if(stateless)
                _slSession.Delete(entity);
            else
                _sfSession.Delete(entity);
        }

        /// <summary>
        /// Gets an entity by the identifier.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entityID">The entity identifier.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The entity if it found in the datastore; otherwise null.</returns>
        public TEntity Get<TEntity>(object entityID, bool stateless)
            where TEntity : IEntity
        {
            BeginWork(stateless);
            if(stateless)
                return _slSession.Get<TEntity>(entityID);
            else
                return _sfSession.Get<TEntity>(entityID);
        }

        /// <summary>
        /// Loads an entity by the identifier.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entityID">The entity identifier.</param>
        /// <returns>The entity if it found in the datastore; otherwise throws exception.</returns>
        /// <remarks>The exception won't be thrown, if stateless mode is enabled.</remarks>
        public TEntity Load<TEntity>(object entityID)
            where TEntity : IEntity
        {
            BeginWork(false);
            return _sfSession.Load<TEntity>(entityID);
        }

        /// <summary>
        /// Refresh an entity state.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        public void Refresh<TEntity>(TEntity entity, bool stateless)
            where TEntity : IEntity
        {
            BeginWork(stateless);
            if(stateless)
                _slSession.Refresh(entity);
            else
                _sfSession.Refresh(entity);
        }

        /// <summary>
        /// Merges an entity state.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public TEntity Merge<TEntity>(TEntity entity)
            where TEntity : class, IEntity
        {
            BeginWork(false);
            return  _sfSession.Merge(entity);
        }

        /// <summary>
        /// Query over the entity collection.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The queryable entity collection.</returns>
        public IQueryable<TEntity> Query<TEntity>(bool stateless)
            where TEntity : IEntity
        {
            BeginWork(stateless);
            if(stateless)
                return _slSession.Query<TEntity>();
            else
                return _sfSession.Query<TEntity>();
        }

        /// <summary>
        /// Creates an ORM query.
        /// </summary>
        /// <param name="queryString">The query string.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The ORM query.</returns>
        public IQuery OrmQuery(string queryString, bool stateless)
        {
            BeginWork(stateless);
            if(stateless)
                return _slSession.CreateQuery(queryString);
            else
                return _sfSession.CreateQuery(queryString);
        }

        /// <summary>
        /// Creates a SQL query.
        /// </summary>
        /// <param name="queryString">The query string.</param>
        /// <param name="stateless">A value indicates whether the stateless mode is enabled.</param>
        /// <returns>The SQL query.</returns>
        public ISQLQuery SqlQuery(string queryString, bool stateless)
        {
            BeginWork(stateless);
            if(stateless)
                return _slSession.CreateSQLQuery(queryString);
            else
                return _sfSession.CreateSQLQuery(queryString);
        }

        /// <summary>
        /// Suspends the unit.
        /// </summary>
        public void Suspend()
        {
            if(_sfTransaction != null || _slTransaction != null)
                throw Err.Of(this).Message("The work was not commited or rolled back").Ex();

            if(_sfSession != null && _sfSession.IsConnected)
                _sfSession.Disconnect();
            if(_slSession != null)
            {
                _slSession.Dispose();
                _slSession = null;
            }
        }

        /// <summary>
        /// Disposes the associated session(s).
        /// </summary>
        public void Dispose()
        {
            if(_slSession != null)
            {
                if(_slTransaction != null)
                    _slTransaction.Dispose();
                _slSession.Dispose();
                _slSession = null;
            }
            if(_sfSession != null)
            {
                if(_sfTransaction != null)
                    _sfTransaction.Dispose();
                _sfSession.Dispose();
                _sfSession = null;
            }
        }

        #endregion

        #region Utilities

        private void BeginWork(bool stateless)
        {
            if(stateless)
            {
                if(_slSession == null)
                    _slSession = _factory.OpenStatelessSession();
                if(_slTransaction == null)
                    _slTransaction = _slSession.BeginTransaction();
            }
            else
            {
                if(_sfSession == null)
                {
                    _sfSession = _factory.OpenSession();
                    _sfSession.FlushMode = FlushMode.Commit;
                }
                else if(!_sfSession.IsConnected)
                {
                    _sfSession.Reconnect();
                }
                if(_sfTransaction == null)
                    _sfTransaction = _sfSession.BeginTransaction();
            }
        }

        #endregion
    }
}
